簡單介紹全域管理工具 Zustand
現在 react 的相關套件裡面最使用最廣泛的全域管理工具應該是 Redux,在工作上較大的專案也都是使用 Redux 在做狀態管理,但是我想要介紹另外一個近期討論度很高的狀態管理工具,Zustand。
npm install zustand
oryarn add zustand
Zustand 的使用非常簡單,我們先從 zustand
裡面引入 create 來建立一個 store,用來貯存狀態跟改變狀態用的 object,object 裡面則會有狀態的 value 跟改變他的 function。
// zustand
import { create } from "zustand";
export interface CountState {
count: number;
increase: (by: number) => void;
resetCount: () => void;
}
export interface NumberState {
number: number;
increase: (by: number) => void;
resetNumber: () => void;
}
export const useCountStore = create<CountState>()((set) => ({
count: 0,
increase: (by) => set((state) => ({ count: state.count + by })),
resetCount: () => set({ count: 0 }),
}));
export const useNumberStore = create<NumberState>()((set) => ({
number: 0,
increase: (by) => set((state) => ({ number: state.number + by })),
resetNumber: () => set({ number: 0 }),
}));
這邊我建立了兩個不同的 store 並且一起做 export 的動作。
create 這個 function 接收一個 function,這個 function 會接到一個 set function,透過 set function 來更新我們的 state,透過這個 set function 來更新 state 的方式就好像我們透過 setState((state) => newState)
的方式相同,需要回傳一個新的 value。
然後在 App.tsx 裡面引入剛剛建立的兩個 store。
// zustand
import {
useCountStore,
useNumberStore,
CountState,
NumberState,
} from "./store";
type Props = {
onClick: () => void;
children: string;
};
function Button({ onClick, children }: Props) {
return <button onClick={onClick}>{children}</button>;
}
function DeepChild() {
const { count, increase, resetCount } = useCountStore<CountState>(
(state) => state
);
return (
<div>
<h3>DeepChild</h3>
<p>Count:{count}</p>
<Button onClick={() => increase(1)}>increase</Button>
<Button onClick={resetCount}>reset</Button>
</div>
);
}
const Child = function Child() {
return (
<div>
<h2>Child</h2>
<DeepChild />
</div>
);
};
function ParentOne() {
const { count } = useCountStore<CountState>((state) => state);
console.log("ParentOne render");
return (
<div style={{ border: "solid 1px black" }}>
<h1>Here is Parent One</h1>
<p>Count:{count}</p>
<Child />
</div>
);
}
function ParentTwo() {
console.log("ParentTwo render");
const { number, increase, resetNumber } = useNumberStore<NumberState>(
(state) => state
);
return (
<div style={{ border: "solid 1px black" }}>
<h1>Here is Parent Two</h1>
<p>Number:{number}</p>
<Button onClick={() => increase(1)}>increase</Button>
<Button onClick={resetNumber}>reset</Button>
</div>
);
}
function App() {
return (
<div style={{ display: "flex", gap: "20px" }}>
<ParentOne />
<ParentTwo />
</div>
);
}
這樣就完成了,就是這麼簡單。
透過 Zustand 可以簡單的完成狀態管理,不需要使用 Provider 把元件包起來,只需要在需要的地方引入你的 store 就可以直接使用。
Zustand 裡面也可以很簡單的做到非同步的狀態更新。
在 create 的 function 裡面除了可以接到 set function 以外還可以接到另外一個 get function,set 是用來更新我們的狀態,get 則是可以在任何地方取得目前最新的狀態。
// store.ts
import { create } from "zustand";
type Data = {
id: string;
full_name: string;
html_url: string;
};
export interface DataState {
data: Data[];
status: string;
getData: (query: string) => void;
}
export const useDataStore = create<DataState>()((set, get) => ({
data: [],
status: "Idle",
getData: async (query) => {
set({ status: "Loading", data: [] });
console.log("get", get());
const res = await fetch(
`https://api.github.com/search/repositories?q=${query}`
);
const data = await res.json();
set({ status: "Success", data: data.items });
console.log("get", get());
},
}));
在 create 的時候建立一個 async 非同步的 function 可以在裡面做非同步的動作,這邊用 fetch github 上面的 repo 來做範例,使用 getData 的時候需要傳入一個 query 字串,然後會把資料存到 data 裡面。
並且給 state 一個狀態可以用來顯示畫面,另外可以注意一下 console.log("get", get())
裡面的內容,都會是當前最新的狀態。
// app.tsx
import { useState } from "react";
import { useDataStore } from "./store";
function App() {
const { data, status, getData } = useDataStore((state) => state);
const [value, setValue] = useState("");
function clickHandler() {
if (value) getData(value);
}
return (
<div>
<h1>Fetch with zustand</h1>
<input
type="text"
value={value}
onChange={({ target }) => setValue(target.value)}
/>
<button onClick={clickHandler}>Search</button>
{status === "Idle" && <p>No Data.</p>}
{status === "Loading" && <p>Loading...</p>}
{status === "Success" && (
<ul>
{data.map((item) => (
<li key={item.id}>
<h2>{item.full_name}</h2>
<p>
Repo Url:
<a
href={item.html_url}
target="_blank"
rel="noreferrer noopener"
>
{item.html_url}
</a>
</p>
</li>
))}
</ul>
)}
</div>
);
}
然後在 App 這邊簡單透過 useState 來控制 input 當用戶點擊的時候就進行搜尋的動作。
透過 Zustand 就可以做到把狀態跟元件分開,在狀態的檔案裡面專心處理取得資料跟更新狀態的邏輯,然後元件的地方依照不同的狀態顯示畫面。
雖然大部分的第三方狀態管理工具也可以做到相同的事情,但是 Zustand 把這個事情做到非常簡化,只要建立,然後引入就可以直接使用了。
redux 搭配 toolkit 雖然也可以讓流程簡化到非常相似,但是當遇到非同步的狀態管理時還是需要再另外仰賴 thunk 或是 sage 才可以另外做到非同步的狀態更新。
下一篇簡單介紹非同步的狀態管理工具 react query。
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium